/*
 * Distributed as part of mchange-commons-java 0.2.11
 *
 * Copyright (C) 2015 Machinery For Change, Inc.
 *
 * Author: Steve Waldman <swaldman@mchange.com>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of EITHER:
 *
 *     1) The GNU Lesser General Public License (LGPL), version 2.1, as 
 *        published by the Free Software Foundation
 *
 * OR
 *
 *     2) The Eclipse Public License (EPL), version 1.0
 *
 * You may choose which license to accept if you wish to redistribute
 * or modify this work. You may offer derivatives of this work
 * under the license you have chosen, or you may provide the same
 * choice of license which you have been offered here.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * You should have received copies of both LGPL v2.1 and EPL v1.0
 * along with this software; see the files LICENSE-EPL and LICENSE-LGPL.
 * If not, the text of these licenses are currently available at
 *
 * LGPL v2.1: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 *  EPL v1.0: http://www.eclipse.org/org/documents/epl-v10.php 
 * 
 */

package com.mchange.v2.beans;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import com.mchange.v2.log.*;

import com.mchange.v2.lang.Coerce;

public final class BeansUtils
{
    final static MLogger logger = MLog.getLogger( BeansUtils.class );

    final static Object[] EMPTY_ARGS = new Object[0];

    public static PropertyEditor findPropertyEditor( PropertyDescriptor pd )
    {
        PropertyEditor out = null;
        Class editorClass = null;
        try
        {
            editorClass = pd.getPropertyEditorClass();
            if (editorClass != null)
                out = (PropertyEditor) editorClass.newInstance();
        }
        catch (Exception e)
        {
//          e.printStackTrace();
//          System.err.println("WARNING: Bad property editor class " + editorClass.getName() + 
//          " registered for property " + pd.getName());
            if (logger.isLoggable( MLevel.WARNING ) )
                logger.log(MLevel.WARNING, "Bad property editor class " + editorClass.getName() + " registered for property " + pd.getName(), e);
        }

        if ( out == null )
            out = PropertyEditorManager.findEditor( pd.getPropertyType() );
        return out;
    }

    public static boolean equalsByAccessibleProperties( Object bean0, Object bean1 )
    throws IntrospectionException
    { return equalsByAccessibleProperties( bean0, bean1, Collections.EMPTY_SET ); }

    public static boolean equalsByAccessibleProperties( Object bean0, Object bean1, Collection ignoreProps )
	throws IntrospectionException
    {
        Map m0 = new HashMap();
        Map m1 = new HashMap();
        extractAccessiblePropertiesToMap( m0, bean0, ignoreProps );
        extractAccessiblePropertiesToMap( m1, bean1, ignoreProps );
        //System.err.println("Map0 -> " + m0);
        //System.err.println("Map1 -> " + m1);
        return m0.equals(m1);
    }

    public static boolean equalsByAccessiblePropertiesVerbose( Object bean0, Object bean1, Collection ignoreProps )
	throws IntrospectionException
    {
        Map m0 = new HashMap();
        Map m1 = new HashMap();
        extractAccessiblePropertiesToMap( m0, bean0, ignoreProps );
        extractAccessiblePropertiesToMap( m1, bean1, ignoreProps );

	boolean out = true;

	//System.err.println("m0 keys:");
	//for ( Iterator ii = m0.keySet().iterator(); ii.hasNext(); )
	//    System.err.println( ii.next() );

	if ( m0.size() != m1.size() )
	{
	    System.err.println( "Unequal sizes --> Map0: " + m0.size() + "; m1: " + m1.size() );
	    Set s0extras = m0.keySet();
	    s0extras.removeAll( m1.keySet() );

	    Set s1extras = m1.keySet();
	    s1extras.removeAll( m0.keySet() );

	    if (s0extras.size() > 0)
	    {
		System.err.println("Map0 extras:");
		for (Iterator ii = s0extras.iterator(); ii.hasNext(); )
		    System.err.println( '\t' + ii.next().toString() );
	    }
	    if (s1extras.size() > 0)
	    {
		System.err.println("Map1 extras:");
		for (Iterator ii = s1extras.iterator(); ii.hasNext(); )
		    System.err.println( '\t' + ii.next().toString() );
	    }
	    out = false;
	}
	for ( Iterator ii = m0.keySet().iterator(); ii.hasNext(); )
	{
	    String key = (String) ii.next();
	    Object val0 = m0.get( key );
	    Object val1 = m1.get( key );
	    if ( (val0 == null && val1 != null) || (val0 != null && ! val0.equals( val1 )) )
	    {
		System.err.println( '\t' + key + ": " + val0 + " != " + val1 );
		out = false;
	    }
	}

	return out;
    }

    public static void overwriteAccessibleProperties( Object sourceBean, Object destBean )
	throws IntrospectionException
    { overwriteAccessibleProperties( sourceBean, destBean, Collections.EMPTY_SET ); }

    public static void overwriteAccessibleProperties( Object sourceBean, Object destBean, Collection ignoreProps )
	throws IntrospectionException
    {
        try
        {
            BeanInfo beanInfo = Introspector.getBeanInfo( sourceBean.getClass(), Object.class ); //so we don't see message about getClass()
            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
            for( int i = 0, len = pds.length; i < len; ++i)
            {
                PropertyDescriptor pd = pds[i];
                if ( ignoreProps.contains( pd.getName() ) )
                    continue;

                Method getter = pd.getReadMethod();
                Method setter = pd.getWriteMethod();

                if ( getter == null || setter == null )
                {
                    if ( pd instanceof IndexedPropertyDescriptor )
                    {
//                      System.err.println("WARNING: BeansUtils.overwriteAccessibleProperties() does not");
//                      System.err.println("support indexed properties that do not provide single-valued");
//                      System.err.println("array getters and setters! [The indexed methods provide no means");
//                      System.err.println("of modifying the size of the array in the destination bean if");
//                      System.err.println("it does not match the source.]");

                        if ( logger.isLoggable( MLevel.WARNING ) )
                            logger.warning("BeansUtils.overwriteAccessibleProperties() does not" +
                                            " support indexed properties that do not provide single-valued" +
                                            " array getters and setters! [The indexed methods provide no means" +
                                            " of modifying the size of the array in the destination bean if" +
                            " it does not match the source.]");
                    }

                    //System.err.println("Property inaccessible for overwriting: " + pd.getName());
                    if (logger.isLoggable( MLevel.INFO ))
                        logger.info("Property inaccessible for overwriting: " + pd.getName());
                }
                else
                {
                    Object value = getter.invoke( sourceBean, EMPTY_ARGS );
                    setter.invoke( destBean, new Object[] { value } );
                }
            }
        }
        catch ( IntrospectionException e )
        { throw e; }
        catch ( Exception e )
        {
            //e.printStackTrace();
            if (Debug.DEBUG && Debug.TRACE >= Debug.TRACE_MED && logger.isLoggable( MLevel.FINE ))
                logger.log( MLevel.FINE, "Converting exception to throwable IntrospectionException" );

            throw new IntrospectionException( e.getMessage() );
        }
    }

    public static void overwriteAccessiblePropertiesFromMap( Map sourceMap, Object destBean, boolean skip_nulls )
    throws IntrospectionException
    { overwriteAccessiblePropertiesFromMap( sourceMap, destBean, skip_nulls, Collections.EMPTY_SET ); }

    public static void overwriteAccessiblePropertiesFromMap( Map sourceMap, Object destBean, boolean skip_nulls, Collection ignoreProps )
    throws IntrospectionException
    {
        overwriteAccessiblePropertiesFromMap( sourceMap, 
                        destBean, 
                        skip_nulls, 
                        ignoreProps, 
                        false,
                        MLevel.WARNING,
                        MLevel.WARNING,
                        true);
    }

    public static void overwriteAccessiblePropertiesFromMap( Map sourceMap, 
                    Object destBean, 
                    boolean skip_nulls, 
                    Collection ignoreProps, 
                    boolean coerce_strings,
                    MLevel cantWriteLevel,
                    MLevel cantCoerceLevel,
                    boolean die_on_one_prop_failure)
    throws IntrospectionException
    {
        if (cantWriteLevel == null)
            cantWriteLevel = MLevel.WARNING;
        if (cantCoerceLevel == null)
            cantCoerceLevel = MLevel.WARNING;

        Set sourceMapProps = sourceMap.keySet();

        String propName = null;
        BeanInfo beanInfo = Introspector.getBeanInfo( destBean.getClass(), Object.class ); //so we don't see message about getClass()
        PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
        //System.err.println("ignoreProps: " + ignoreProps );
        for( int i = 0, len = pds.length; i < len; ++i)
        {
            PropertyDescriptor pd = pds[i];
            propName = pd.getName();

            if (! sourceMapProps.contains( propName ))
                continue;

            if ( ignoreProps != null && ignoreProps.contains( propName ) )
            {
                //System.err.println("ignoring: " + propName);
                continue;
            }
            //else
            //    System.err.println("not ignoring: " + propName);

            Object propVal = sourceMap.get( propName );
            if (propVal == null)
            {
                if (skip_nulls) continue;
                //do we need to worry about primitives here?
            }

            Method setter = pd.getWriteMethod();
            boolean rethrow = false;

            Class propType = pd.getPropertyType();;

//          try
//          {

            if ( setter == null )
            {
                if ( pd instanceof IndexedPropertyDescriptor )
                {
                    if ( logger.isLoggable( MLevel.FINER ) )
                        logger.finer("BeansUtils.overwriteAccessiblePropertiesFromMap() does not" +
                                        " support indexed properties that do not provide single-valued" +
                                        " array getters and setters! [The indexed methods provide no means" +
                                        " of modifying the size of the array in the destination bean if" +
                        " it does not match the source.]");

                }

                if ( logger.isLoggable( cantWriteLevel ))
                {
                    String msg = "Property inaccessible for overwriting: " + propName; 
                    logger.log( cantWriteLevel, msg );
                    if (die_on_one_prop_failure)
                    {
                        rethrow = true;
                        throw new IntrospectionException( msg );
                    }
                }

            }
            else
            {
                if (coerce_strings &&
                                propVal != null && 
                                propVal.getClass() == String.class && 
                                (propType = pd.getPropertyType()) != String.class &&
                                Coerce.canCoerce( propType ))
                {
                    Object coercedPropVal;
                    try
                    { 
                        coercedPropVal = Coerce.toObject( (String) propVal, propType ); 
                        //System.err.println(propName + "-> coercedPropVal: " + coercedPropVal);
                        setter.invoke( destBean, new Object[] { coercedPropVal } );
                    }
                    catch (IllegalArgumentException e) 
                    {
                        // thrown by Coerce.toObject()
                        // recall that NumberFormatException inherits from IllegalArgumentException
                        String msg = 
                            "Failed to coerce property: " + propName +
                            " [propVal: " + propVal + "; propType: " + propType + "]";
                        if ( logger.isLoggable( cantCoerceLevel ) )
                            logger.log( cantCoerceLevel, msg, e );
                        if (die_on_one_prop_failure)
                        {
                            rethrow = true;
                            throw new IntrospectionException( msg );
                        }
                    }
                    catch (Exception e)
                    {
                        String msg = 
                            "Failed to set property: " + propName +
                            " [propVal: " + propVal + "; propType: " + propType + "]";
                        if ( logger.isLoggable( cantWriteLevel ) )
                            logger.log( cantWriteLevel, msg, e );
                        if (die_on_one_prop_failure)
                        {
                            rethrow = true;
                            throw new IntrospectionException( msg );
                        }
                    }
                }
                else
                {
                    try
                    {
                        //System.err.println("invoking method: " + setter);
                        setter.invoke( destBean, new Object[] { propVal } );
                    }
                    catch (Exception e)
                    {
                        String msg = 
                            "Failed to set property: " + propName +
                            " [propVal: " + propVal + "; propType: " + propType + "]";
                        if ( logger.isLoggable( cantWriteLevel ) )
                            logger.log( cantWriteLevel, msg, e );
                        if (die_on_one_prop_failure)
                        {
                            rethrow = true;
                            throw new IntrospectionException( msg );
                        }
                    }
                }
            }
//          }
//          catch (Exception e)
//          {
//          if (e instanceof IntrospectionException && rethrow)
//          throw (IntrospectionException) e;
//          else
//          {
//          String msg = 
//          "An exception occurred while trying to set property '" + propName +
//          "' to value '" + propVal + "'. ";
//          logger.log(MLevel.WARNING, msg, e);
//          if (die_on_one_prop_failure)
//          {
//          rethrow = true;
//          throw new IntrospectionException( msg + e.toString());
//          }
//          }
//          }
        }
    }

    public static void appendPropNamesAndValues(StringBuffer appendIntoMe, Object bean, Collection ignoreProps) throws IntrospectionException
    {
        Map tmp = new TreeMap( String.CASE_INSENSITIVE_ORDER );
        extractAccessiblePropertiesToMap( tmp, bean, ignoreProps );
        boolean first = true;
        for (Iterator ii = tmp.keySet().iterator(); ii.hasNext(); )
        {
            String key = (String) ii.next();
            Object val = tmp.get( key );
            if (first)
                first = false;
            else
                appendIntoMe.append( ", " );
            appendIntoMe.append( key );
            appendIntoMe.append( " -> ");
            appendIntoMe.append( val );
        }
    }


    public static void extractAccessiblePropertiesToMap( Map fillMe, Object bean ) throws IntrospectionException
    { extractAccessiblePropertiesToMap( fillMe, bean, Collections.EMPTY_SET ); }

    public static void extractAccessiblePropertiesToMap( Map fillMe, Object bean, Collection ignoreProps ) throws IntrospectionException
    {
        String propName = null;
        try
        {
            BeanInfo bi = Introspector.getBeanInfo( bean.getClass(), Object.class );
            PropertyDescriptor[] pds = bi.getPropertyDescriptors();
            for (int i = 0, len = pds.length; i < len; ++i)
            {
                PropertyDescriptor pd = pds[i];
                propName = pd.getName();
                if (ignoreProps.contains( propName ))
                    continue;

                Method readMethod = pd.getReadMethod();
                Object propVal = readMethod.invoke( bean, EMPTY_ARGS );
                fillMe.put( propName, propVal );
            }
        }
        catch ( IntrospectionException e )
        {
//          if (propName != null)
//          System.err.println("Problem occurred while overwriting property: " + propName);
            if ( logger.isLoggable( MLevel.WARNING ) )
                logger.warning("Problem occurred while overwriting property: " + propName);
            if (Debug.DEBUG && Debug.TRACE >= Debug.TRACE_MED && logger.isLoggable( MLevel.FINE ))
                logger.logp( MLevel.FINE, 
                                BeansUtils.class.getName(),
                                "extractAccessiblePropertiesToMap( Map fillMe, Object bean, Collection ignoreProps )",
                                (propName != null ? "Problem occurred while overwriting property: " + propName : "") + " throwing...",
                                e );
            throw e; 
        }
        catch ( Exception e )
        {
            //e.printStackTrace();
            if (Debug.DEBUG && Debug.TRACE >= Debug.TRACE_MED && logger.isLoggable( MLevel.FINE ))
                logger.logp( MLevel.FINE, 
                                BeansUtils.class.getName(),
                                "extractAccessiblePropertiesToMap( Map fillMe, Object bean, Collection ignoreProps )",
                                "Caught unexpected Exception; Converting to IntrospectionException.",
                                e );
            throw new IntrospectionException( e.toString() + (propName == null ? "" : " [" + propName + ']') );
        }
    }

    private static void overwriteProperty( String propName, Object value, Method putativeSetter, Object target )
    throws Exception
    {
        if ( putativeSetter.getDeclaringClass().isAssignableFrom( target.getClass() ) )
            putativeSetter.invoke( target, new Object[] { value } );
        else
        {
            BeanInfo beanInfo = Introspector.getBeanInfo( target.getClass(), Object.class );
            PropertyDescriptor pd = null;

            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
            for( int i = 0, len = pds.length; i < len; ++i)
                if (propName.equals( pds[i].getName() ))
                {
                    pd = pds[i];
                    break;
                }

            Method targetSetter = pd.getWriteMethod();
            targetSetter.invoke( target, new Object[] { value } );
        }
    }


    public static void overwriteSpecificAccessibleProperties( Object sourceBean, Object destBean, Collection props )
    throws IntrospectionException
    {
        try
        {
            Set _props = new HashSet(props);

            BeanInfo beanInfo = Introspector.getBeanInfo( sourceBean.getClass(), Object.class ); //so we don't see message about getClass()
            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
            for( int i = 0, len = pds.length; i < len; ++i)
            {
                PropertyDescriptor pd = pds[i];
                String name = pd.getName();
                if (! _props.remove( name ) )
                    continue;

                Method getter = pd.getReadMethod();
                Method setter = pd.getWriteMethod();

                if ( getter == null || setter == null )
                {
                    if ( pd instanceof IndexedPropertyDescriptor )
                    {
//                      System.err.println("WARNING: BeansUtils.overwriteAccessibleProperties() does not");
//                      System.err.println("support indexed properties that do not provide single-valued");
//                      System.err.println("array getters and setters! [The indexed methods provide no means");
//                      System.err.println("of modifying the size of the array in the destination bean if");
//                      System.err.println("it does not match the source.]");

                        if ( logger.isLoggable( MLevel.WARNING ) )
                            logger.warning("BeansUtils.overwriteAccessibleProperties() does not" +
                                            " support indexed properties that do not provide single-valued" +
                                            " array getters and setters! [The indexed methods provide no means" +
                                            " of modifying the size of the array in the destination bean if" +
                            " it does not match the source.]");
                    }

                    if ( logger.isLoggable( MLevel.INFO ) )
                        logger.info("Property inaccessible for overwriting: " + pd.getName());
                }
                else
                {
                    Object value = getter.invoke( sourceBean, EMPTY_ARGS );
                    overwriteProperty( name, value, setter, destBean );
                    //setter.invoke( destBean, new Object[] { value } );
                }
            }
            if ( logger.isLoggable( MLevel.WARNING ) )
            {
                for (Iterator ii = _props.iterator(); ii.hasNext(); )
                    logger.warning("failed to find expected property: " + ii.next());
                //System.err.println("failed to find expected property: " + ii.next());
            }
        }
        catch ( IntrospectionException e )
        { throw e; }
        catch ( Exception e )
        {
            //e.printStackTrace();
            if (Debug.DEBUG && Debug.TRACE >= Debug.TRACE_MED && logger.isLoggable( MLevel.FINE ))
                logger.logp( MLevel.FINE, 
                                BeansUtils.class.getName(),
                                "overwriteSpecificAccessibleProperties( Object sourceBean, Object destBean, Collection props )",
                                "Caught unexpected Exception; Converting to IntrospectionException.",
                                e );
            throw new IntrospectionException( e.getMessage() );
        }
    }

    public static void debugShowPropertyChange( PropertyChangeEvent evt )
    {
        System.err.println("PropertyChangeEvent: [ propertyName -> " + evt.getPropertyName() + 
                        ", oldValue -> " + evt.getOldValue() +
                        ", newValue -> " + evt.getNewValue() +
        " ]");
    }

    private BeansUtils()
    {}
}
